package com.ziiwee.reduce.spring;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.metadata.ConstraintDescriptor;
import java.nio.charset.Charset;
import java.util.*;

/**
 * @author ziiwee
 * @date 2020/8/19
 */
@Component
@Order(-1)
public class ValidHandlerExceptionResolver implements HandlerExceptionResolver {

    private final Log log = LogFactory.getLog(getClass());

    private final ObjectMapper objectMapper;

    public ValidHandlerExceptionResolver(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        //TODO 时间还有时区问题，需要换一个解决方案
        try {
            if (ex instanceof ConstraintViolationException) {
                return resolveConstraintViolationException(request, response, (ConstraintViolationException) ex);
            }
            if (ex instanceof MethodArgumentNotValidException) {
                return resolveMethodArgumentNotValidException(request, response, (MethodArgumentNotValidException) ex);
            }
        } catch (Exception e) {
            log.error("处理异常时发生错误 : " + e.getMessage(), e);
        }
        return null;
    }

    private ModelAndView resolveMethodArgumentNotValidException(HttpServletRequest request, HttpServletResponse response, MethodArgumentNotValidException ex) {
        List<ConstraintViolation<?>> errors = new ArrayList<>();
        BindingResult bindingResult = ex.getBindingResult();
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            errors.add(fieldError.unwrap(ConstraintViolation.class));
        }
        writeErrorMessage(errors, request, response);
        return new ModelAndView();
    }

    private ModelAndView resolveConstraintViolationException(HttpServletRequest request, HttpServletResponse response, ConstraintViolationException ex) {
        writeErrorMessage(ex.getConstraintViolations(), request, response);
        return new ModelAndView();
    }

    private void writeErrorMessage(Collection<ConstraintViolation<?>> violations, HttpServletRequest request, HttpServletResponse response) {
        try {
            List<ErrorInfo> errorInfos = new ArrayList<>();
            for (ConstraintViolation<?> v : violations) {
                ConstraintDescriptor<?> descriptor = v.getConstraintDescriptor();
                errorInfos.add(new ErrorInfo(
                        v.getRootBeanClass().getSimpleName(),
                        v.getPropertyPath().toString(),
                        v.getInvalidValue(),
                        v.getMessage(),
                        descriptor.getAnnotation().annotationType().getSimpleName(),
                        descriptor.getAttributes()
                ));
            }
            Error error = new Error(
                    HttpStatus.BAD_REQUEST,
                    request.getServletPath(),
                    errorInfos);
            String json = objectMapper.writeValueAsString(error);
            response.setStatus(HttpStatus.BAD_REQUEST.value());
            response.setContentType(MediaType.APPLICATION_JSON.toString());
            response.setCharacterEncoding(Charset.defaultCharset().name());
            response.getWriter().println(json);
        } catch (Exception e) {
            log.error("写入错误消息失败 : " + e.getMessage(), e);
        }
    }

    @Data
    public static class Error {
        private Date timestamp = new Date();
        private int status;
        private String error;
        private String message;
        private String path;
        private List<ErrorInfo> errorInfos;

        public Error(HttpStatus status, String path, List<ErrorInfo> errorInfos) {
            this.status = status.value();
            this.error = status.getReasonPhrase();
            this.path = path;
            this.errorInfos = errorInfos;
        }
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class ErrorInfo {
        private String domain;
        private String field;
        private Object value;
        private String message;
        private String rule;
        private Map<String, Object> ruleParameters;
    }

}
